package com.hmdp.utils;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author 32179
 */
@Component
public class RedisUtil {
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    private final StringRedisTemplate stringRedisTemplate;

    @Value("${redis-util.key-prefix}")
    private String keyPrefix;

    @Value("${redis-util.null-value}")
    private String nullValue;

    @Value("${redis-util.null-value-expire}")
    private Long nullValueExpire;

    public RedisUtil(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public <T> void set(String key, T value) {
        key = keyPrefix + key;
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value));
    }

    public <T> void set(String key, T value, Long expireTime, TimeUnit timeUnit) {
        key = keyPrefix + key;
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expireTime, timeUnit);
    }

    public <T> void setWithLogicalExpireTime(String key, T value, Long expireTime, TimeUnit timeUnit) {
        LocalDateTime now = LocalDateTime.now();
        key = keyPrefix + key;
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(now.plusSeconds(timeUnit.toSeconds(expireTime)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public <T> T get(String key, Class<T> type, DbCallback<T> dbCallback) {
        key = keyPrefix + key;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (json != null) {
            if (json.equals(nullValue)) {
                return null;
            } else {
                return JSONUtil.toBean(json, type);
            }
        }
        boolean isLock = tryLock(key);
        if (isLock) {
            try {
                json = stringRedisTemplate.opsForValue().get(key);
                if (json != null) {
                    if (json.equals(nullValue)) {
                        return null;
                    } else {
                        return JSONUtil.toBean(json, type);
                    }
                }
                T value = dbCallback.execute();
                if (value == null) {
                    set(key, nullValue, nullValueExpire, TimeUnit.SECONDS);
                } else {
                    set(key, value);
                }
                return value;
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unLock(key);
            }
        } else {
            try {
                Thread.sleep(100);
                return get(key, type, dbCallback);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public <T> T get(String key, Class<T> type, DbCallback<T> dbCallback, Long expireTime, TimeUnit timeUnit) {
        key = keyPrefix + key;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (json != null) {
            if (json.equals(nullValue)) {
                return null;
            } else {
                return JSONUtil.toBean(json, type);
            }
        }
        boolean isLock = tryLock(key);
        if (isLock) {
            try {
                json = stringRedisTemplate.opsForValue().get(key);
                if (json != null) {
                    if (json.equals(nullValue)) {
                        return null;
                    } else {
                        return JSONUtil.toBean(json, type);
                    }
                }
                T value = dbCallback.execute();
                if (value == null) {
                    set(key, nullValue, nullValueExpire, TimeUnit.SECONDS);
                } else {
                    set(key, value, expireTime, timeUnit);
                }
                return value;
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unLock(key);
            }
        } else {
            try {
                Thread.sleep(100);
                return get(key, type, dbCallback, expireTime, timeUnit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    // 逻辑过期解决缓存击穿问题
    public <T> T getWithLogicalExpireTime(String key, Class<T> type, DbCallback<T> dbCallback, Long expireTime, TimeUnit timeUnit) {
        key = keyPrefix + key;
        LocalDateTime now = LocalDateTime.now();
        String json = stringRedisTemplate.opsForValue().get(key);
        if (json != null) {
            if (json.equals(nullValue)) {
                return null;
            } else {
                RedisData redisData = JSONUtil.toBean(json, RedisData.class);
                T data = JSONUtil.toBean((JSONObject) redisData.getData(), type);
                if (redisData.getExpireTime().isBefore(now)) {
                    boolean isLock = tryLock(key);
                    if (isLock) {
                        String finalKey = key;
                        json = stringRedisTemplate.opsForValue().get(finalKey);
                        if (json != null) {
                            if (json.equals(nullValue)) {
                                return null;
                            } else {
                                if (redisData.getExpireTime().isAfter(now)) {
                                    return data;
                                }
                            }
                        }
                        CACHE_REBUILD_EXECUTOR.submit(() -> {
                            try {
                                T newData = dbCallback.execute();
                                if (newData == null) {
                                    set(finalKey, nullValue, nullValueExpire, TimeUnit.SECONDS);
                                } else {
                                    setWithLogicalExpireTime(finalKey, newData, expireTime, timeUnit);
                                }
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            } finally {
                                unLock(finalKey);
                            }
                        });
                    }
                }
                return data;
            }
        } else {
            boolean isLock = tryLock(key);
            if (isLock) {
                try {
                    json = stringRedisTemplate.opsForValue().get(key);
                    if (json != null) {
                        if (json.equals(nullValue)) {
                            return null;
                        } else {
                            RedisData redisData = JSONUtil.toBean(json, RedisData.class);
                            if (redisData.getExpireTime().isAfter(now)) {
                                return JSONUtil.toBean((JSONObject) redisData.getData(), type);
                            }
                        }
                    }
                    T data = dbCallback.execute();
                    if (data == null) {
                        set(key, nullValue, nullValueExpire, TimeUnit.SECONDS);
                    } else {
                        setWithLogicalExpireTime(key, data, expireTime, timeUnit);
                    }
                    return data;
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unLock(key);
                }
            } else {
                try {
                    Thread.sleep(100);
                    return getWithLogicalExpireTime(key, type, dbCallback, expireTime, timeUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    public void del(String key) {
        key = keyPrefix + key;
        stringRedisTemplate.delete(key);
    }

    private boolean tryLock(String key) {
        String lockKey = keyPrefix + "lock:" + key;
        Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock");
        return b != null && b;
    }

    private void unLock(String key) {
        String lockKey = keyPrefix + "lock:" + key;
        stringRedisTemplate.delete(lockKey);
    }

    public interface DbCallback<R> {
        R execute();
    }

    @Setter
    @Getter
    private static class RedisData {
        private Object data;
        private LocalDateTime expireTime;
    }
}
